<?php

/**
 * Database patcher
 * @author b3ha (baloghbh@gmail.com)
 */
class cDbPatcher
{
    const CONFIG                            = 'config.ini';
    const SCRIPT__CREATE_HANDLER_TABLE      = '/sql/create_handler_table.sql';
    const SCRIPT__INSERT_MODULE             = '/sql/insert_module.sql';
    const SCRIPT__EXISTS_MODULE             = '/sql/exists_module.sql';
    const SCRIPT__GET_MODULES               = '/sql/get_modules.sql';
    const VERSION_FILENAME                  = '_version';
    const FOLDER_STRUCTURE                  = 'structure';
    const FOLDER_DATA                       = 'data';
    const FOLDER_ROLLBACK                   = 'rollback';
    
    protected $pdo                          = NULL;
    /**
     * Write informations to console/file
     * @var resource
     */
    protected $outHandler                   = NULL;
    /**
     * @var array
     */
    protected $config                       = NULL;

    /**
     * Construct
     */
    public function __construct(array $config = NULL)
    {
        $this->config = $config ? : parse_ini_file(self::CONFIG, TRUE);

        date_default_timezone_set($this->config['else']['timezone']);
        $time = date('Ymd_His');

        if ($this->config['else']['log_type'] === 'console')
            $this->outHandler = fopen('php://stdout', 'w');
        else
            $this->outHandler = fopen($this->config['paths']['log'] . "dbp__{$time}.log", 'w');

        fwrite($this->outHandler, "Log at: {$time}\n\n");
    }

    /**
     * Run all stuff
     */
    public function run()
    {
        $this->initConnection();
        $this->initHandlerTable();
        $this->initModules();

        $this->runPatches();
    }

    /**
     * Initialize database connection
     */
    public function initConnection()
    {
        fwrite($this->outHandler, "Initializing...\n");
        fwrite($this->outHandler, str_repeat('-', 32) . "\n");

        // Try to connet to the database
        try
        {
            $this->pdo = new PDO(   "mysql:host=" . $this->config['db']['host'] .
                                    ";dbname=" . $this->config['db']['dbname'],
                                    $this->config['db']['username'],
                                    $this->config['db']['password']);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //$this->pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
        }
        catch(PDOException $e)
        {
            fwrite($this->outHandler, "-----ERROR-----\n");
            fwrite($this->outHandler, $e->getMessage() . "\n");
            exit(1);
        }
        fwrite($this->outHandler, "> PDO created.\n");
    }

    /**
     * Initialize the table which contains modules and their version
     */
    public function initHandlerTable()
    {
        // Create our handler table if not exists
        $sql = str_replace('<table_name>', $this->config['else']['table_name'],
                            file_get_contents(__DIR__ . self::SCRIPT__CREATE_HANDLER_TABLE));
        try
        {
            $this->pdo->exec($sql);
        }
        catch(PDOException $e)
        {
            fwrite($this->outHandler, "-----ERROR-----\n");
            fwrite($this->outHandler, $e->getMessage() . "\n");
            exit(1);
        }
        fwrite($this->outHandler, "> Handler table is OK.\n");
    }

    /**
     * Write new modules into the handler table, then read all the modules
     */
    public function initModules()
    {
        $modules = array();

        // Read modules
        foreach (glob($this->config['paths']['application'] . $this->config['paths']['modules'] . '*', GLOB_ONLYDIR) as $module_name)
        {
            if (file_exists($this->config['paths']['application'] .
                            $this->config['paths']['modules'] .
                            basename($module_name) . "/" .
                            $this->config['paths']['module_patches'] .
                            self::VERSION_FILENAME))
            {
                $modules[] = basename($module_name);
                fwrite($this->outHandler, " > Found module: ". basename($module_name) . "\n");
            }
        }
        // "Main" module -> refer to the application/models folder
        if (file_exists($this->config['paths']['application'] .
                        $this->config['paths']['main_models'] .
                        $this->config['paths']['main_model_patches'] .
                        self::VERSION_FILENAME))
        {
            $modules[] = $this->config['else']['main_module_name'];
            fwrite( $this->outHandler,   " > Found module " .
                    $this->config['else']['main_module_name'] . "\n");
        }
        fwrite($this->outHandler, "> Modules readed.\n");


        // Insert those modules into the handler table which doesnt exists
        $module_exists_sql = str_replace('<table_name>', $this->config['else']['table_name'],
                                        file_get_contents(__DIR__ .  self::SCRIPT__EXISTS_MODULE));
        $me_stmt = $this->pdo->prepare($module_exists_sql);

        $insert_module_sql = str_replace('<table_name>', $this->config['else']['table_name'],
                                        file_get_contents(__DIR__ .  self::SCRIPT__INSERT_MODULE));
        $im_stmt = $this->pdo->prepare($insert_module_sql);

        $count_modules = 0;
        foreach ($modules as $module)
        {
            $me_stmt->execute(array(':name' => $module));
            list($module_exists) = $me_stmt->fetch(PDO::FETCH_NUM);

            if ( ! $module_exists)
            {
                    $im_stmt->execute(array(':name' => $module));
                    fwrite($this->outHandler, " > Inserted module: {$module}\n");

                    ++$count_modules;
            }
        }
        if ($count_modules > 0)
        fwrite($this->outHandler, "> Modules inserted: {$count_modules}\n");
        fwrite($this->outHandler, "\n");
    }

    /**
     * In handler table update a module's version number
     *
     * @param string $module
     * @param string $version
     */
    public function updateModuleVersion($module, $version)
    {
        $this->pdo->exec(   "UPDATE " .
                            $this->config['else']['table_name'] .
                            " SET version='{$version}' WHERE name='{$module}';");
    }

    /**
     * Get all the modules and their version
     *
     * @return array keys - name, version
     */
    public function getDbModules()
    {
        $modules = array();

        $sql = str_replace('<table_name>', $this->config['else']['table_name'],
                            file_get_contents(__DIR__ . self::SCRIPT__GET_MODULES));
        foreach ($this->pdo->query($sql) as $row)
        {
            $modules[] = array(	'name'		=> $row['name'],
                                'version'	=> $row['version']);
        }

        return $modules;
    }

    /**
     * Get a modules's file system version
     *
     * @param string $module_name
     * @return string
     */
    public function getModuleVersion($module_name)
    {
        $v 	= NULL;
        $path 	= NULL;

        // set module's patches folder path
        if ($module_name !== $this->config['else']['main_module_name'])
        {
            $path = $this->config['paths']['application'] .
                    $this->config['paths']['modules'] .
                    $module_name . "/" .
                    $this->config['paths']['module_patches'] .
                    self::VERSION_FILENAME;
        }
        else
        {
            $path = $this->config['paths']['application'] .
                    $this->config['paths']['main_models'] .
                    $this->config['paths']['main_model_patches'] .
                    self::VERSION_FILENAME;
        }

        if (file_exists($path))
        {
            $fh = fopen($path, 'r');
            $v = fgets($fh);
            $v = str_replace("\n", '', $v);
            $v = str_replace("\r", '', $v);
            fclose($fh);
        }

        return $v;
    }

    /**
     * Run patches
     */
    public function runPatches()
    {
        fwrite($this->outHandler, "Patching...\n");
        fwrite($this->outHandler, str_repeat('-', 32) . "\n");

        $module_patch_path  = NULL;
        $type               = NULL;
        $modules            = $this->getDbModules();

        foreach ($modules as $module)
        {
            fwrite($this->outHandler, "> Module: {$module['name']}\n");

            // version (file_system)
            $fs_v = $this->getModuleVersion($module['name']);
            if ($fs_v === '') continue;
            // patches versions (folders)
            $patches_v = array();

            // set module's patches folder path
            if ($module['name'] !== $this->config['else']['main_module_name'])
            {
                $module_patch_path =    $this->config['paths']['application'] .
                                        $this->config['paths']['modules'] .
                                        $module['name'] . "/" .
                                        $this->config['paths']['module_patches'];
            }
            else
            {
                $module_patch_path = 	$this->config['paths']['application'] .
                                        $this->config['paths']['main_models'] .
                                        $this->config['paths']['main_model_patches'];
            }

            // Read patch versions
            foreach (glob($module_patch_path . '*', GLOB_ONLYDIR) as $patch_v)
            {
                $patches_v[] = $patch_v;
            }

            // COMMIT
            if (strcmp($fs_v, $module['version']) > 0)
            {
                $final_patches_v = array();
                foreach ($patches_v as $patch_v)
                {
                    if ((strcmp(basename($patch_v), $module['version']) > 0)
                        &&
                        (strcmp(basename($patch_v), $fs_v) <= 0))
                    {
                        $final_patches_v[] = $patch_v;
                    }
                }
                $this->runCommitQuerys($module['name'], $final_patches_v);
            }
            // ROLLBACK
            else if (strcmp($fs_v, $module['version']) < 0)
            {
                $final_patches_v = array();
                foreach ($patches_v as $patch_v)
                {
                    if ((strcmp(basename($patch_v), $module['version']) <= 0)
                        &&
                        (strcmp(basename($patch_v), $fs_v) >= 0))
                    {
                        $final_patches_v[] = $patch_v;
                    }
                }
                $this->runRollBackQuerys($module['name'], $fs_v, array_reverse($final_patches_v));
            }
            // SAME VERSION
            else continue;

            fwrite($this->outHandler, "\n");
        }
    }

    /**
     * Commit
     *
     * @param string $module
     * @param array $patches
     */
    public function runCommitQuerys($module, array $patches)
    {
        fwrite($this->outHandler, " > Commit\n");

        foreach ($patches as $patch)
        {
            $commands = 0;

            if ((file_exists($patch . '/' . self::FOLDER_STRUCTURE . '/'))
                ||
                (file_exists($patch . '/' . self::FOLDER_DATA . '/')))
            fwrite($this->outHandler, "  > " . basename($patch) . "\n");

            //----------------------------------------------------------
            // Structure
            if (file_exists($patch . '/' . self::FOLDER_STRUCTURE . '/'))
            {
                fwrite($this->outHandler, "   > Structure\n");

                try
                {
                    foreach (glob($patch . '/' . self::FOLDER_STRUCTURE . '/*.sql') as $sql_patch)
                    {
                        fwrite($this->outHandler, "    > " . basename($sql_patch) . "\n");
                        $this->pdo->exec(file_get_contents($sql_patch));

                        ++$commands;
                    }
                }
                catch(PDOException $e)
                {
                    fwrite($this->outHandler, "-----ERROR-----\n");
                    fwrite($this->outHandler, $e->getMessage() . "\n");
                    exit(1);
                }
            }

            //----------------------------------------------------------
            // Data
            if (file_exists($patch . '/' . self::FOLDER_DATA . '/'))
            {
                fwrite($this->outHandler, "   > Data\n");

                try
                {
                    $this->pdo->beginTransaction();
                    foreach (glob($patch . '/' . self::FOLDER_DATA . '/*.sql') as $sql_patch)
                    {
                        fwrite($this->outHandler, "    > " . basename($sql_patch) . "\n");
                        $this->pdo->exec(file_get_contents($sql_patch));

                        ++$commands;
                    }
                    $this->pdo->commit();
                }
                catch(PDOException $e)
                {
                    $this->pdo->rollBack();

                    fwrite($this->outHandler, "-----ERROR-----\n");
                    fwrite($this->outHandler, $e->getMessage() . "\n");
                    exit(1);
                }
            }

            //----------------------------------------------------------
            // Update module version (in Db)
            if ($commands > 0)
            {
                $this->updateModuleVersion($module, basename($patch));
                fwrite($this->outHandler, "  > Hurray! Module <{$module}> has patched to version <" . basename($patch) . ">\n");
            }
        }
    }

    /**
     * RollBack
     *
     * @param string $module
     * @param string $fs_v
     * @param array $patches
     */
    public function runRollBackQuerys($module, $fs_v, array $patches)
    {
        fwrite($this->outHandler, " > RollBack\n");

        foreach ($patches as $patch)
        {
            if (strcmp(basename($patch), $fs_v) === 0) break;

            $commands = 0;

            if ((file_exists($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_DATA . '/'))
                ||
                (file_exists($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_STRUCTURE . '/')))
            fwrite($this->outHandler, "  > " . basename($patch) . "\n");

            //----------------------------------------------------------
            // Data
            if (file_exists($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_DATA . '/'))
            {
                fwrite($this->outHandler, "   > Data\n");

                try
                {
                    $this->pdo->beginTransaction();
                    foreach (glob($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_DATA . '/*.sql') as $sql_patch)
                    {
                        fwrite($this->outHandler, "    > " . basename($sql_patch) . "\n");
                        $this->pdo->exec(file_get_contents($sql_patch));

                        ++$commands;
                    }
                    $this->pdo->commit();
                }
                catch(PDOException $e)
                {
                    $this->pdo->rollBack();

                    fwrite($this->outHandler, "-----ERROR-----\n");
                    fwrite($this->outHandler, $e->getMessage() . "\n");
                    exit(1);
                }
            }

            //----------------------------------------------------------
            // Structure
            if (file_exists($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_STRUCTURE . '/'))
            {
                fwrite($this->outHandler, "   > Structure\n");

                try
                {
                    foreach (glob($patch . '/' . self::FOLDER_ROLLBACK . '/' . self::FOLDER_STRUCTURE . '/*.sql') as $sql_patch)
                    {
                        fwrite($this->outHandler, "    > " . basename($sql_patch) . "\n");
                        $this->pdo->exec(file_get_contents($sql_patch));

                        ++$commands;
                    }
                }
                catch(PDOException $e)
                {
                    fwrite($this->outHandler, "-----ERROR-----\n");
                    fwrite($this->outHandler, $e->getMessage() . "\n");
                    exit(1);
                }
            }

            //----------------------------------------------------------
            // Update module version (in Db)
            if ($commands > 0)
            {
                if (next($patches) === FALSE)
                {
                    $this->updateModuleVersion($module, '0.0');
                    fwrite($this->outHandler, "  > Hurray! Module <{$module}> has rollbacked to version <0.0>\n");
                }
                else
                {
                    $this->updateModuleVersion($module, basename(current($patches)));
                    fwrite($this->outHandler, "  > Hurray! Module <{$module}> has rollbacked to version <" . basename(current($patches)) . ">\n");
                }
            }
        }
    }

    /**
     * Destruct
     */
    public function __destruct()
    {
        $this->pdo = NULL;

        fflush($this->outHandler);
        fclose($this->outHandler);
    }
}

# EOF
